package se.teamscape.shared.rpc;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * A database query wrapper. When fetching models by key isn't sufficient, we
 * use this flexible query builder to tell the datastore what we need.
 * 
 * TODO: implement range, caching
 * 
 * @author Andreas Borglin
 * 
 */
@SuppressWarnings("serial")
public class DBQuery implements Serializable {

    // Types
    public static final String SELECT = "select from ";
    public static final String DELETE = "delete from ";

    // Filter operators
    public static final String EQUALS = " == ";
    public static final String NOT_EQUAlS = " != ";
    public static final String GREATER_THAN = " > ";
    public static final String LESS_THAN = " < ";
    public static final String GREATER_THAN_OR_EQUALS = " >= ";
    public static final String LESS_THAN_OR_EQUALS = " <= ";

    // Logical operators
    public static final String AND = " && ";
    public static final String OR = " || ";

    public static final String ASCENDING = " asc ";
    public static final String DESCENDING = " desc ";

    private static final String COMMA_SEPARATOR = ", ";

    private String mType;
    private String mClassName;
    private final List<QueryFilter> mQueryFilters;
    private List<Serializable> mParamValues;
    private String mOrdering;
    private Integer mRangeStart;
    private Integer mRangeEnd;

    static class QueryFilter implements Serializable {
        private String mFieldName;
        private String mOperator;
        private String mParamName;
        private String mParamType;
        private String mLogicalSeparator;

        public QueryFilter() {
        }

        public QueryFilter(String fn, String op, String pn, String pt, String ls) {
            mFieldName = fn;
            mOperator = op;
            mParamName = pn;
            mParamType = pt;
            mLogicalSeparator = ls;
        }
    }

    // Builder methods to build a complete query

    public static DBQuery selectFrom(String className) {
        return new DBQuery(DBQuery.SELECT, className);
    }

    public static DBQuery deleteFrom(String className) {
        return new DBQuery(DBQuery.DELETE, className);
    }

    public DBQuery where(String classField, String fieldType, String operator, String value) {
        addFilter(classField, operator, classField + "Param", fieldType, value);
        return this;
    }

    public DBQuery and() {
        addLogicalOperator(DBQuery.AND);
        return this;
    }

    public DBQuery or() {
        addLogicalOperator(DBQuery.OR);
        return this;
    }

    public DBQuery orderBy(String field, String order) {
        addOrdering(field, order);
        return this;
    }

    public DBQuery withRange(int start, int end) {
        setRange(start, end);
        return this;
    }

    // End builder methods

    protected DBQuery() {
        mQueryFilters = new ArrayList<QueryFilter>();
    }

    protected DBQuery(String cl) {
        this();
        mClassName = cl;
        mType = DBQuery.SELECT;
    }

    protected DBQuery(String type, String cl) {
        this(type);
        mClassName = cl;
    }

    @Override
    public String toString() {

        StringBuffer buffer = new StringBuffer();

        buffer.append(mType);
        buffer.append(mClassName);

        if (mQueryFilters.size() > 0) {

            buffer.append(" where ");

            // Add filters
            for (QueryFilter filter : mQueryFilters) {

                buffer.append(filter.mFieldName);
                buffer.append(filter.mOperator);
                buffer.append(filter.mParamName);

                if (filter.mLogicalSeparator != null)
                    buffer.append(filter.mLogicalSeparator);
            }

            buffer.append(" parameters ");

            // Add param declarations
            boolean addComma = false;
            for (QueryFilter filter : mQueryFilters) {

                if (addComma)
                    buffer.append(", ");
                buffer.append(filter.mParamType);
                buffer.append(" ");
                buffer.append(filter.mParamName);
                addComma = true;
            }
        }

        // Add ordering
        if (mOrdering != null) {
            buffer.append(" order by ");
            buffer.append(mOrdering);
        }

        return buffer.toString();
    }

    protected void addFilter(String fieldName, String operator, String paramName, String paramType, String paramValue) {
        addFilter(fieldName, operator, paramName, paramType, paramValue, null);
    }

    protected void addFilter(String fieldName, String operator, String paramName, String paramType, String paramValue,
            String logical) {
        addFilter(new QueryFilter(fieldName, operator, paramName, paramType, logical));

        if (mParamValues == null)
            mParamValues = new ArrayList<Serializable>();
        mParamValues.add(paramValue);
    }

    private void addFilter(QueryFilter filter) {
        mQueryFilters.add(filter);
    }

    private void addLogicalOperator(String logical) {
        QueryFilter lastFilter = mQueryFilters.get(mQueryFilters.size() - 1);
        lastFilter.mLogicalSeparator = logical;
    }

    public void setType(String type) {
        mType = type;
    }

    public String getType() {
        return mType;
    }

    protected void setClassName(String cl) {
        this.mClassName = cl;
    }

    public String getClassName() {
        return mClassName;
    }

    public List<Serializable> getParamValues() {
        return mParamValues;
    }

    protected void addOrdering(String fieldName, String order) {
        if (this.mOrdering == null)
            this.mOrdering = fieldName.trim() + order;
        else
            this.mOrdering += COMMA_SEPARATOR + fieldName.trim() + order;
    }

    public String getOrdering() {
        return mOrdering;
    }

    protected void setRange(int start, int end) {
        mRangeStart = start;
        mRangeEnd = end;
    }

    public int getRangeStart() {
        return mRangeStart;
    }

    public int getRangeEnd() {
        return mRangeEnd;
    }

}
